RNN과 LSTM을 이해해보자_180415_11_21.html RNN 과 RNN의 일종인 LSTM 에 대해 알아보겠다. 우선 두 알고리즘의 개요를 간략히 언급한 뒤 foward, backward compute pass를 살펴보겠다. 기본적으로 CS231n 강좌를 참고했다. http://cs231n.stanford.edu/syllabus.html forward, backward pass 관련 설명과 그림은 제가 직접 만들었음을 밝힌다. GRU 가 궁금하신 분은 이곳을 참고하면된다. https://ratsgo.github.io/deep%20learning/2017/05/13/GRU/ @ RNN의 기본 구조 img 923c0301-5535-428d-8278-a9b4b4ef592e RNN은 히든 노드가 방향을 가진 엣지로 연결돼 순환구조를 이루는(directed cycle) 인공신경망의 한 종류입니다. 시퀀스 길이(ex. 문장의 길이)에 관계없이 인풋과 아웃풋을 받아들일 수 있는 네트워크 구조이다. 따라서, 필요에 따라 다양하고 유연하게 구조를 만들 수 있다는 점이 RNN의 가장 큰 장점이다. @ img cd944288-5495-4732-8ec6-78f9e98c88d6 RNN의 기본 구조는 위 그림과 같다. 녹색 박스는 히든 "state from hidden" 를 의미한다. 빨간 박스는 input x, 파란 박스는 output y 이다. $$$h_{t}$$$(현재 상태의 히든 state) 는 $$$h_{t−1}$$$(직전 시점의 히든 state) 를 받아 갱신된다. $$$y_{t}$$$(현재 상태의 output) 은 $$$h_{t}$$$ 를 전달받아 갱신되는 구조이다. 수식에서도 알 수 있듯 hidden state 의 activation function 은 비선형 함수인 tanh 이다. 그런데 activation function 으로 왜 비선형 함수를 쓰는걸까요? 밑바닥부터 시작하는 딥러닝의 글귀를 하나 인용해 보겠습니다. 선형 함수인 h(x)=cx를 활성 함수로 사용한 3층 네트워크를 떠올려 보세요. 이를 식으로 나타내면 y(x)=h(h(h(x)))가 됩니다. 이 계산은 y(x)=c∗c∗c∗x처럼 세번의 곱셈을 수행하지만 실은 y(x)=ax와 똑같은 식입니다. a=c3이라고만 하면 끝이죠. 즉 히든레이어가 없는 네트워크로 표현할 수 있습니다. 그래서 층을 쌓는 혜택을 얻고 싶다면 활성함수로는 반드시 비선형함수를 사용해야 합니다. @ RNN의 기본 구조 RNN의 기본 동작을 살펴 보기 위해 CS231n 강좌의 Karpathy 가 든 예제를 살펴보자. 어떤 글자가 주어졌을 때 바로 다음 글자를 예측하는 character-level-model을 만든다고 가정한다. 예컨대 RNN 모델에 ‘hell’을 넣으면 ‘o’를 반환하게 해 결과적으로는 ‘hello’를 출력하게 만들고 싶은 것이다. 우선 우리가 가진 학습데이터의 글자는 ‘h’, ‘e’, ‘l’, ‘o’ 네 개뿐이다. 이를 one-hot-vector로 바꾸면 각각 [1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]이 된다. img 508fb521-6e5a-4a42-82da-1df672f4101b $$$x_{1}$$$ (input character: h) 은 [1,0,0,0] 이다. 이를 기반으로 $$$h_{1}$$$ 인 [0.3,−0.1,0.9]를 만들었다 $$$h_{0}$$$ 는 존재하지 않기 때문에 랜덤 값을 집어넣는다. 이를 바탕으로 $$$y_{1}$$$ 을 [1.0,2.2,−3.0,4.1] 로 생성한다. 마찬가지로 두번째, 세번째, 네번째 단계들도 모두 갱신하게된다. 이 과정을 foward propagation 이라고 부른다. 다른 인공신경망과 마찬가지로 RNN도 정답을 필요로 한다. 모델에 정답을 알려줘야 모델이 parameter를 적절히 갱신해 나갈 수 있기 때문이다. 이 경우엔 바로 다음 글자가 정답이 된다. 예컨대 ‘h’의 다음 정답은 ‘e’, ‘e’ 다음은 ‘l’, ‘l’ 다음은 ‘l’, ‘l’ 다음은 ‘o’가 정답입니다. 위의 그림을 기준으로 설명을 하면, 첫번째 정답인 ‘e’는 두번째 요소만 1이고 나머지가 0인 one-hot-vector 이다. 그림을 보면 아웃풋에 진한 녹색으로 표시된 숫자들이 있는데 정답에 해당하는 인덱스를 의미한다. 이 정보를 바탕으로 backpropagation 을 수행해 parameter 들을 갱신해 나간다. 그렇다면 RNN이 학습하는 parameter는 무엇일까? input x를 hidden layer h로 보내는 W_xh, previous hidden layer h에서 next hidden layer h로 보내는 W_hh, hidden layer h에서 output y로 보내는 W_hy, 가 바로 parameter 이다. 그리고 모든 시점의 state에서 이 parameter는 동일하게 적용된다(shared weights). @ RNN의 순전파 앞장에서 말씀드린 RNN의 기본 구조를 토대로 forward compute pass를 아래와 같이 그려봤습니다. 위에서 설명한 수식을 그래프로 옮겨놓은 것일 뿐입니다. img 1df1ccf8-9574-481f-838f-bdf424f4634e @ RNN의 역전파 자, 이제 backward pass를 볼까요? 아래 그림과 같습니다. 혹시 역전파가 생소하신 분은 이곳을 참고하시기 바랍니다. https://ratsgo.github.io/deep%20learning/2017/05/14/backprop/ img 79e43d0f-28f7-44fe-b2bf-831a3b30cf6a 위 움짤과 아래 그림은 같은 내용입니다. 우선 forward pass를 따라 최종 출력되는 결과는 $$$y_{t}$$$입니다. 최종 loss L 에 대한 yt의 그래디언트 (effect of L on $$$y_{t}$$$) $$$\frac{dL}{dy_{t}}$$$ 가 RNN의 역전파 연산에서 가장 먼저 등장합니다. 이를 편의상 $$$dy_{t}$$$ 라고 표기했고, 순전파 결과 $$$y_{t}$$$ 와 대비해 붉은색으로 표시했습니다. 앞으로 이 표기를 따를 예정입니다. backpropagation을 할때, $$$dy_{t}$$$ 는 덧셈 그래프를 타고 양방향에 모두 그대로 분배가 됩니다. $$$dW_{hy}$$$ 는 흘러들어온 그래디언트 $$$dy_{t}$$$ 에 로컬 그래디언트 $$$h_{t}$$$ 를 곱해 구합니다. $$$dh_{t}$$$ 는 흘러들어온 그래디언트 $$$dy_{t}$$$ 에 $$$W_{hy}$$$ 를 곱한 값입니다. $$$dh_{raw}$$$ 는 흘러들어온 그래디언트인 $$$dh_{t}$$$ 에 로컬 그래디언트인 $$$1−tanh^{2}(h_{raw})$$$을 곱해 구합니다. 나머지도 동일한 방식으로 구합니다. img e2d2dc92-309a-440e-9840-6679ead648f2 다만 아래 그림에 주의할 필요가 있다. RNN은 히든 노드가 순환 구조를 띄는 신경망이다. 즉 $$$h_{t}$$$ 를 만들 때 $$$h_{t−1}$$$ 가 반영된다. 바꿔 말하면 아래 그림의 $$$dh_{t−1}$$$ 은 t-1 시점의 Loss에서 흘러들어온 그래디언트인 $$$W_{hy}∗dy_{t−1}$$$뿐 아니라 ★에 해당하는 그래디언트 또한 더해져 동시에 반영된다는 뜻입니다. @ LSTM의 기본 구조 img f9e12f99-8653-48f5-a607-9a36d3f94579 RNN은 관련 정보와 그 정보를 사용하는 지점 사이 거리가 멀 경우 역전파시 그래디언트가 점차 줄어 학습능력이 크게 저하되는 것으로 알려져 있습니다. 이를 vanishing gradient problem이라고 합니다. 이 문제를 극복하기 위해서 고안된 것이 바로 LSTM입니다. LSTM은 RNN의 히든 state에 cell-state를 추가한 구조입니다. LSTM을 가장 쉽게 시각화한 포스트: http://colah.github.io/posts/2015-08-Understanding-LSTMs/ img cba43454-7bcd-4b88-9be9-6a872f564c8e cell state는 일종의 컨베이어 벨트 역할을 합니다. 덕분에 state가 꽤 오래 경과하더라도 그래디언트가 비교적 전파가 잘 되게 됩니다. LSTM 셀의 수식은 아래와 같습니다. ⊙는 element-wise product(요소별 곱셈)을 뜻하는 Hadamard product 연산자입니다. \begin{align*} { f }_{ t }&=\sigma ({ W }_{ xh\_ f }{ x }_{ t }+{ W }_{ hh\_ f }{ h }_{ t-1 }+{ b }_{ h\_ f })\\ { i }_{ t }&=\sigma ({ W }_{ xh\_ i }{ x }_{ t }+{ W }_{ hh\_ i }{ h }_{ t-1 }+{ b }_{ h\_ i })\\ { o }_{ t }&=\sigma ({ W }_{ xh\_ o }{ x }_{ t }+{ W }_{ hh\_ o }{ h }_{ t-1 }+{ b }_{ h\_ o })\\ { g }_{ t }&=\tanh { ({ W }_{ xh\_ g }{ x }_{ t }+{ W }_{ hh\_ g }{ h }_{ t-1 }+{ b }_{ h\_ g }) } \\ { c }_{ t }&={ f }_{ t }\odot { c }_{ t-1 }+{ i }_{ t }\odot { g }_{ t }\\ { h }_{ t }&={ o }_{ t }\odot \tanh { ({ c }_{ t }) } \end{align*} $$$f_{t}$$$(forget gate) 는 ‘과거 정보를 잊기’를 위한 게이트이다. $$$h_{t−1}$$$과 $$$x_{t}$$$를 받아 시그모이드를 취해준 값이 바로 forget gate가 내보내는 값이 됩니다. $$${ f }_{ t }=\sigma ({ W }_{ xh\_ f }{ x }_{ t }+{ W }_{ hh\_ f }{ h }_{ t-1 }+{ b }_{ h\_ f })$$$ 시그모이드 함수의 출력 범위는 0에서 1 사이이기 때문에 그 값이 0이라면 이전 상태의 정보는 잊고, 1이라면 이전 상태의 정보를 온전히 기억하게 됩니다. input gate $$$i_{t}⊙g_{t}$$$는 ‘현재 정보를 기억하기’ 위한 게이트입니다. $$$h_{t−1}$$$과 $$$x_{t}$$$를 받아 시그모이드를 취한다. 또 같은 입력인 $$$h_{t−1}$$$과 $$$x_{t}$$$에 하이퍼볼릭탄젠트를 취해준다. 이 둘의 값을 Hadamard product 연산을 한 값이 바로 input gate가 내보내는 값이 됩니다. 개인적으로 $$$i_{t}$$$의 범위는 0~1, $$$g_{t}$$$의 범위는 -1~1이기 때문에 각각 강도와 방향을 나타낸다고 이해했습니다. Illustrations for forget gate and input gate img edfa9519-0a4c-4354-ae71-3bb6494c4a54 @ LSTM의 순전파 LSTM 순전파는 아래와 같습니다. img 63d2e2f5-0786-4154-b43f-70e6a2e5780a 여기서 주목해야 할 점은 가운데에 있는 $$$H_{t}$$$입니다. 이 행렬을 행 기준으로 4등분해 f,i,g,o 각각에 해당하는 활성함수를 적용하는 방식으로 f,i,g,o 를 계산합니다. (물론 이렇게 계산하지 않고 다른 방식을 써도 관계는 없습니다) 이를 그림으로 나타내면 다음과 같습니다. img a65105c3-792c-4ee3-bb8c-b0d6b0ca068b @ LSTM의 역전파 img 3aa2dd61-9f75-46c4-81cb-4d49ca7dfc65 아래 나열할 그림은 위 움짤과 내용이 같습니다. 우선 $$$d_{f_{t}},d_{i_{t}},d_{g_{t}},d_{o_{t}}$$$를 구하기까지 backward pass는 RNN과 유사합니다. img 0f27e6d1-73fe-4208-9640-c0cc0f512d93 $$$dH_{t}$$$ 를 구하는 과정이 LSTM backward pass 핵심이라고 할 수 있죠. $$$H_{t}$$$는 $$$i_{t},f_{t},o_{t},g_{t}$$$로 구성된 행렬입니다. 바꿔 말하면 각각에 대한 그래디언트를 구하고 이들을 합치면(merge) dHt를 만들 수 있다는 뜻입니다. i,f,o의 활성함수는 시그모이드이고, g만 하이퍼볼릭탄젠트입니다. 각각의 활성함수에 대한 로컬 그래디언트를 구해 흘러들어온 그래디언트를 곱해주면 됩니다. img 18c3cd99-f5bc-48a7-8d7f-9edd83b48353 순전파 과정에서 Ht를 4등분해 $$$i_{t},f_{t},o_{t},g_{t}$$$ 를 구했던 것처럼, backward pass에서는 $$$d_{i},d_{f},d_{o},d_{g}$$$ 를 다시 합쳐 $$$dH_{t}$$$ 를 만듭니다. 이렇게 구한 $$$dH_{t}$$$ 는 다시 RNN과 같은 방식으로 역전파가 되는 구조입니다. img 0c6d5230-0052-4f2d-8ccd-a559c6cdf0f5 LSTM은 cell state와 히든 state가 재귀적으로 구해지는 네트워크입니다. 따라서 cell state의 그래디언트와 히든 state의 그래디언트는 직전 시점의 그래디언트 값에 영향을 받습니다. 이는 RNN과 마찬가지입니다. 이를 역전파시 반영해야 합니다. @ 파이썬 구현 CS231n 강좌에 파이썬 numpy 패키지만 활용해 구현해놓은 RNN 코드가 있습니다. https://gist.github.com/karpathy/d4dee566867f8291f086 여기서 loss function 만 바꾸면 LSTM 구조로 변경할 수 있다. 글자나 단어 단위로 학습하기 위한 LSTM loss function 를 만들어봤다. 위 설명과 notation이 약간 다르긴 한데, 본질적으로는 완전히 같은 코드입니다. https://gist.github.com/ratsgo/6e9a094c7108dee8147ef0a13666de47#file-lstm_loss-py def lossFun(inputs, targets, hprev, cprev): xs, hs, cs, is_, fs, os, gs, ys, ps= {}, {}, {}, {}, {}, {}, {}, {}, {} hs[-1] = np.copy(hprev) # t=0일때 t-1 시점의 hidden state가 필요하므로 cs[-1] = np.copy(cprev) loss = 0 H = hidden_size # forward pass for t in range(len(inputs)): xs[t] = np.zeros((vocab_size, 1)) xs[t][inputs[t]] = 1 tmp = np.dot(Wxh, xs[t]) + np.dot(Whh, hs[t - 1]) + bh # hidden state is_[t] = sigmoid(tmp[:H]) fs[t] = sigmoid(tmp[H:2 * H]) os[t] = sigmoid(tmp[2 * H: 3 * H]) gs[t] = np.tanh(tmp[3 * H:]) cs[t] = fs[t] * cs[t-1] + is_[t] * gs[t] hs[t] = os[t] * np.tanh(cs[t]) # compute loss for i in range(len(targets)): idx = len(inputs) - len(targets) + i ys[idx] = np.dot(Why, hs[idx]) + by # unnormalized log probabilities for next chars ps[idx] = np.exp(ys[idx]) / np.sum(np.exp(ys[idx])) # probabilities for next chars loss += -np.log(ps[idx][targets[i], 0]) # softmax (cross-entropy loss) # backward pass: compute gradients going backwards dWxh, dWhh, dWhy = np.zeros_like(Wxh), np.zeros_like(Whh), np.zeros_like(Why) dbh, dby = np.zeros_like(bh), np.zeros_like(by) dhnext, dcnext = np.zeros_like(hs[0]), np.zeros_like(cs[0]) n = 1 a = len(targets) - 1 for t in reversed(range(len(inputs))): if n > len(targets): continue dy = np.copy(ps[t]) dy[targets[a]] -= 1 # backprop into y dWhy += np.dot(dy, hs[t].T) dby += dy dh = np.dot(Why.T, dy) + dhnext # backprop into h dc = dcnext + (1 - np.tanh(cs[t]) * np.tanh(cs[t])) * dh * os[t] # backprop through tanh nonlinearity dcnext = dc * fs[t] di = dc * gs[t] df = dc * cs[t-1] do = dh * np.tanh(cs[t]) dg = dc * is_[t] ddi = (1 - is_[t]) * is_[t] * di ddf = (1 - fs[t]) * fs[t] * df ddo = (1 - os[t]) * os[t] * do ddg = (1 - np.tanh(gs[t]) * np.tanh(gs[t])) * dg da = np.hstack((ddi.ravel(),ddf.ravel(),ddo.ravel(),ddg.ravel())) dWxh += np.dot(da[:,np.newaxis],xs[t].T) dWhh += np.dot(da[:,np.newaxis],hs[t-1].T) dbh += da[:, np.newaxis] dhnext = np.dot(Whh.T, da[:, np.newaxis]) n += 1 a -= 1 for dparam in [dWxh, dWhh, dWhy, dbh, dby]: np.clip(dparam, -5, 5, out=dparam) # clip to mitigate exploding gradients return loss, dWxh, dWhh, dWhy, dbh, dby, hs[len(inputs) - 1], cs[len(inputs) - 1] @ 파일럿 실험 이광수 장편소설 ‘무정’에 위 코드를 실험해봤습니다. ‘무정’은 32만 어절로 이뤄진 작품입니다. 1917년 작품이며 한자어와 대화체 문장이 많습니다. 형식은, 아뿔싸! 내가 어찌하여 이러한 생각을 하는가, 내 마음이 이렇게 약하던가 하면서 두 주먹을 불끈 쥐고 전신에 힘을 주어 이러한 약한 생각을 떼어 버리려 하나, 가슴속에는 이상하게 불길이 확확 일어난다. 이때에, “미스터 리, 어디로가는가” 하는 소리에 깜짝 놀라 고개를 들었다. (중략) 형식은 얼마큼 마음에 수치한 생각이 나서 고개를 돌리며, “아직 그런말에 익숙지를 못해서……” 하고 말끝을 못 맺는다. “대관절 어디로 가는 길인가? 급하지 않거든 점심이나 하세그려.” “점심은 먹었는걸.” “그러면 맥주나 한잔 먹지.” “내가 술을 먹는가.” (중략) “요― 오메데토오(아― 축하하네). 이이나즈케(약혼한사람)가 있나 보네 그려. 음나루호도(그러려니). 그러구도 내게는 아무 말도 없단 말이야. 에, 여보게”하고 손을 후려친다. 이 텍스트를 글자 단위로 one-hot-vector로 바꾼다. 그리고 LSTM에 넣어 학습시켜 본다. hyper-parameters: number_of_hidden_dimension=100 learning_rate=0.1 Result of training Iter 0 : 랫萬게좁뉘쁠름끈玄른작밭裸觀갈나맡文플조바늠헝伍下잊볕홀툽뤘혈調記운피悲렙司狼독벗칼둡걷착날完잣老엇낫業4改‘촉수릎낯깽잊쯤죽道넌友련친씌았융타雲채發造거크휘탁亨律與命텐암먼헝평琵헤落유리벤産이馨텐 Iter 4900: 를왔다내 루방덩이종 은얼에는 집어흔영채는아무 우선을 에서가며 건들하아버전는 애양을자에 운 모양이 랐다. 은 한다선과 ‘마는 .식세식가들어 , 형식다 “내었다.있이문 Iter750000 : 으로 유안하였다. 더할까하는 세상이 솔이요, 알고 게식도 들어울는 듯하였다. 태에그려 깔깔고 웃는듯이 흔반다. 우선형은사람을 어려보낸다. “그려가?” 한다. 영채는손을 기쁘 Iter1000000 : 에 돌내면서, “여러 넣어오습데다. 그 말대 아무도좀 집림과 시오 백매, 저는 열녀더러, 기런 소년이가아니라.” “어리지요.” 노파도 놀라며, “저희마다가말없습니까.” “아니 (대화체) 오랜 시간 학습시켰음에도 뜻 모를 글자들을 내뱉고 있다. 다만 ‘영채’, ‘선형’ 등 무정 인물명들을 언급하거나, 따옴표를 써서 대화체 문장을 구성하거나, ‘-요’ ‘-까’ ‘-다’ 같은 종결어미를 사용해 문장을 끝맺고 있는 등 잘 하고 있는 점도 있다.